package scaladuct

import _root_.util.logging.Logged
import _root_.util.{ParameterizedTypeWrapper, InstanceState, StateHandler}
import hint.HasHints
import l10n.HasHintsMetaDataSourceProvider
import java.net.URL
import collection.JavaConversions.JMapWrapper
import collection.Iterable
import l10n._
import lists.{ListForProperty, ListsManager, DefaultListsManager, ListsSource}
import scala.collection.mutable.Map
import net.sf.cglib.core.{Predicate, NamingPolicy}
import org.objectweb.asm._
import collection.immutable.List
import java.lang.annotation.Annotation
import java.lang.reflect.{InvocationTargetException, Field, Constructor, Method}
import java.util.concurrent.locks.ReentrantLock
import java.io._
import net.sf.cglib.proxy._
import java.lang.{StringBuilder, Class, String}
import scaladuct.hint.HintsHelper
import java.util.{ArrayList, Set}
import scaladuct.DefaultTypeInfo

/**
 * <p>
 * Created by IntelliJ IDEA.
 * User: zslajchrt
 * Date: Aug 11, 2010
 * Time: 5:48:13 PM
 */

object PhaseMethodInterceptor {
  //  type PageFunction = (Class[_], AnyRef *) => ConfigPage[_]
  //  type LookupFunction = (String) => List[PageFunction]
  //  type PageFunction = LookupFunctions.PageFunction
  //  type LookupFunction = LookupFunctions.LookupFunction

  def apply[X](wrappedStarter: Starter[X]): PhaseMethodInterceptor[X] = {
    val factory: Factory = wrappedStarter.asInstanceOf[Factory]
    factory.getCallback(0).asInstanceOf[PhaseMethodInterceptor[X]]
  }

  def apply[X](selectedStarterClass: Class[_],
               args: AnyRef*)
              (implicit configPageFnLookup: LookupFunctions.LookupFunction = LookupFunctions.noLookup): PhaseMethodInterceptor[X] = {
    apply[X]("", selectedStarterClass, args: _*)(configPageFnLookup)
  }

  def apply[X](pagesPrefix: String,
               selectedStarterClass: Class[_],
               args: AnyRef*)
              (implicit configPageFnLookup: LookupFunctions.LookupFunction): PhaseMethodInterceptor[X] = {
    val starterClassResource: URL = loadClassResource(selectedStarterClass)
    apply[X](pagesPrefix, selectedStarterClass, starterClassResource, args: _*)(configPageFnLookup)
  }

  def apply[X](pagesPrefix: String,
               selectedStarterClass: Class[_],
               starterClassResource: URL,
               args: AnyRef*)
              (implicit configPageFnLookup: LookupFunctions.LookupFunction): PhaseMethodInterceptor[X] = {

    require(selectedStarterClass.getConstructors.length == 1, "It is assumed the starter class has only one public constructor. (" + selectedStarterClass.getConstructors.length + ")")

    val interceptor = new PhaseMethodInterceptor[X](configPageFnLookup,
      pagesPrefix, starterClassResource, selectedStarterClass, args: _*)

    interceptor.wrappedStarterInstance = createStarter(selectedStarterClass, interceptor, args: _*)
    interceptor
  }

  //  def apply[X](particle: Particle,
  //               pagesPrefix: String,
  //               selectedStarterClass: Class[_]): PhaseMethodInterceptor[X] = {
  //    val starterClassResource: URL = loadClassResource(selectedStarterClass)
  //    apply[X](pagesPrefix, selectedStarterClass, starterClassResource)(new ParticleConfigPageFunctionLookup(particle))
  //  }

  //  def apply[X](particle: Particle,
  //               pagesPrefix: String,
  //               selectedStarterClass: Class[_],
  //               starterClassResource: URL): PhaseMethodInterceptor[X] = {
  //    apply[X](pagesPrefix, selectedStarterClass, starterClassResource)(new ParticleConfigPageFunctionLookup(particle))
  //  }

  def createStarter[X](selectedStarterClass: Class[_], interceptor: Callback, args: AnyRef*): Starter[X] = {
    val e = new Enhancer()
    e.setSuperclass(selectedStarterClass)
    e.setInterfaces(Array(classOf[Serialization]))
    e.setCallback(interceptor)

    val constructors: Array[Constructor[_]] = selectedStarterClass.getConstructors()
    constructors.find(_.getParameterTypes.length == args.length) match {
      case Some(constructor) => {
        val parTypes: Array[Class[_]] = constructor.getParameterTypes
        if (parTypes.length == 0)
          e.create().asInstanceOf[Starter[X]]
        else {
          e.create(parTypes, Array(args: _*)).asInstanceOf[Starter[X]]
        }
      }
      case None => {
        val sb = new StringBuilder()
        args.foreach(sb.append(_).append(","))
        throw new IllegalArgumentException("No constructor found for starter class " + selectedStarterClass.getName + "(" + sb.toString + ")")
      }
    }
  }

  private def constructClassResourcePath(className: String): String = {
    className.replace('.', '/') + ".class"
  }

  def loadClassResource[X](selectedStarterClass: Class[_]): URL = {
    val starterClassResource: URL =
      selectedStarterClass.getClassLoader.getResource(constructClassResourcePath(selectedStarterClass.getName))
    starterClassResource
  }

}

////////////////////////////////


@serializable
@SerialVersionUID(1L)
class StarterSerializationProxy(val starterState: InstanceState,
                                val interceptor: PhaseMethodInterceptor[_],
                                val starterConstrArgs: AnyRef*) {
  def readResolve(): AnyRef = {
    val starterClass: Class[_] = Thread.currentThread.getContextClassLoader.loadClass(starterState.className)
    val newStarter = PhaseMethodInterceptor.createStarter(starterClass, interceptor, starterConstrArgs: _*)
    starterState.applyState(newStarter, starterClass, classOf[Starter[_]])
    newStarter
  }
}


////////////////////////////////

class ParticleConfigPageFunctionLookup(rootParticle: Particle) extends
Function1[String, Option[(Class[_], Any *) => ConfigPage[_]]] {
  private val particleAsMap = BeanUtils.createBeanInfo(rootParticle)

  def apply(particleName: String) = {
    particleAsMap.get(particleName) match {
      case None => None
      case Some(p) => {
        val particle = p.asInstanceOf[Particle]
        val particleCfgPageFn: (Class[_], Any *) => ConfigPage[_] = (resultClass: Class[_], args) => {
          particle.newConfig(resultClass)
        }

        Some(particleCfgPageFn)
      }
    }
  }
}

class SuspendedCallSignal[X](val interceptor: PhaseMethodInterceptor[X]) extends RuntimeException

@serializable
@SerialVersionUID(1L)
class PhaseMethodInterceptor[X](private val externalConfigPageFnLookup: LookupFunctions.LookupFunction,
                                pagesPrefix: String,
                                starterClassResource: URL,
                                val selectedStarterClass: Class[_],
                                starterConstrArgs: AnyRef*
                                //                                ,
                                //                                val wrappedStarterInstanceGetter: () => Starter[X]
                                 ) extends MethodInterceptor with Logged {
  type ConfigPageFn = LookupFunctions.PageFunction
  type ConfigPageLookupFn = LookupFunctions.LookupFunction

  private val configPageFnLookup: LookupFunctions.LookupFunction =
    if (this.isInstanceOf[LookupFunctions.LookupFunction])
      new DuplexLookupFunction(externalConfigPageFnLookup, this.asInstanceOf[LookupFunctions.LookupFunction])
    else
      externalConfigPageFnLookup

  private[this] var phases: List[Phase] = List.empty

  private[this] var currentPhaseName: String = null

  // todo:
  var removeUnusedPhases = false

  def phaseName() = currentPhaseName

  val pageIdPrefix: String = pagesPrefix

  private var wrappedStarterInstance: Starter[X] = null

  def starter(): Starter[X] = wrappedStarterInstance

  private lazy val noConfigInstance: Option[X] = {
    try {
      val instance: X = wrappedStarterInstance.newInstance
      Some(instance.asInstanceOf[X])
    }
    catch {
      case suspendedCall: SuspendedCallSignal[_] => {
        None
      }
    }
    //    if (instance == SuspendedCall.returnValue)
    //      None
    //    else
    //      Some(instance.asInstanceOf[X])
  }

  private def doInitCallOfNewInstance(): Option[X] = noConfigInstance

  /**
   * Class MethodProxy is not serializable. This is the reason why we must capture it after the interceptor's
   * de-serialization for the purposes of the Phase class. The Phase class initiates the capture process when
   * it finds out that its transient mProxy field is null. Capturing is based on re-invoking the phase method within
   * a special mode indicated by flag captureMethodProxyMode. 
   */
  def captureMethodProxy(methodProxy: MethodProxy): Unit = {
    enterMethodProxyCaptureMode
    try {
      if (captureMethodProxyMode) {
        throw new MethodProxyHolder(methodProxy)
      }
    }
    finally {
      leaveMethodProxyCaptureMode
    }
  }

  private def writeReplace(starter: AnyRef): AnyRef = {
    new StarterSerializationProxy(
      StateHandler.extractState(starter, starter.getClass.getSuperclass, classOf[Starter[_]]),
      this, starterConstrArgs: _*)
  }

  def intercept(instance: AnyRef, method: Method, args: Array[AnyRef], methodProxy: MethodProxy): AnyRef = {
    if (method.getName.equals("writeReplace")) {
      writeReplace(instance)
    } else {

      captureMethodProxy(methodProxy)

      if (method.getName() == "newInstance" && args.length == 0) {
        // Note: The first invocation of invokeSuper on the methodProxy does not call the starter's newInstance method.
        // Instead it calls the proxy-method again and it is why it comes here again. Only then the second invocation
        // goes to the starter's newInstance method.
        val noConfigInst: AnyRef = methodProxy.invokeSuper(instance, args) // it may throw the SuspendedCallSignal exception
        // As the SuspendedCallSignal exception hasn't been thrown there is no phase method in the starter.
        currentPhaseName = getPhaseNameForMethod(method)
        val newPhase = Phase.createNonConfigurablePhase(instance, currentPhaseName, method, methodProxy, args, noConfigInst.asInstanceOf[X])
        phases = List(newPhase)
        noConfigInst
      } else if (method.getName.startsWith("phase") && args.length > 0) {
        currentPhaseName = getPhaseNameForMethod(method)
        //        if (!isAlreadyConfigured(currentPhaseName)) {
        val (cookie, metaCookie) = createCookie(instance, method, args, methodProxy)

        val bundleName = instance.getClass.getSuperclass.getName
        val newPhase = Phase.createConfigurablePhase(currentPhaseName, method, methodProxy, args, metaCookie,
          cookie, bundleName, instance)
        //phases = newPhase :: phases

        throw new SuspendedCallSignal[X](this) // suspend the phase method invocation
        //        } else {
        //          methodProxy.invokeSuper(instance, getRecordedArguments(currentPhaseName))
        //        }
      } else {
        methodProxy.invokeSuper(instance, args)
      }
    }
  }

  private def getPhaseNameForMethod(method: Method): String = {
    val size = phases.filter(phase => phase.method == method).size
    if (size == 0) {
      method.getName
    } else {
      method.getName + "[" + size + "]"
    }
  }

  private def findPhase(phaseName: String): Option[Phase] = {
    phases.find(phase => phase.phaseName == phaseName)
  }

  private def findPhaseOrThrowException(phaseName: String): Phase = {
    findPhase(phaseName) match {
      case None => throw new IllegalArgumentException("Phase " + phaseName + " not found")
      case Some(phase) => phase
    }
  }

  private def isAlreadyConfigured(phaseName: String): Boolean = {
    if (phases == Nil)
      false
    else
      findPhase(phaseName) != None
  }

  private def getRecordedArguments(phaseName: String): Array[AnyRef] = {
    findPhaseOrThrowException(phaseName).phaseMethodArgs
  }

  private def createCookie(instance: AnyRef, phaseMethod: Method, phaseMethodArgs: Array[AnyRef],
                           methodProxy: MethodProxy): (AnyRef, MetaCookie) = {
    cookieArgument(phaseMethod, phaseMethodArgs) match {
      case Some(cookie) => {
        (cookie, new MetaCookie(List(new ArgPropertyValue(0, "property0", phaseMethodArgs.apply(0))), 0, true))
      }
      case None => {
        val cookieClassName = instance.getClass.getSuperclass.getName.replace('.', '_') + '_' + phaseMethod.getName
        val loader: ExpressCookieClassLoader = cookieClassLoader(cookieClassName, phaseMethod, phaseMethodArgs)

        val ctxCookie = populateCookie(loader.metaCookie.contextProperties, loader.loadContextClass,
          loader.contextByteCode, phaseMethodArgs)
        val cookie = populateCookie(loader.metaCookie.cookieProperties, loader.loadCookieClass,
          loader.cookieByteCode, phaseMethodArgs)

        (cookie, loader.metaCookie)
      }
    }
  }

  private def cookieArgument(phaseMethod: Method, phaseMethodArgs: Array[AnyRef]): Option[AnyRef] = {
    if (phaseMethod.getParameterTypes.size == 1 && isCookieType(phaseMethod.getParameterTypes()(0)) && phaseMethodArgs(0) != null) {
      // references are not considered cookies
      if (phaseMethod.getParameterAnnotations()(0).exists(_.isInstanceOf[Ref]))
        None
      else
        Some(phaseMethodArgs(0))
    } else None
  }

  class Assignable(targetClass: Class[_]) {
    def <<(sourceClass: Class[_]): Boolean = targetClass.isAssignableFrom(sourceClass)

    def !<(sourceClass: Class[_]): Boolean = !(<<(sourceClass))
  }

  implicit def convertToAssignable(cls: Class[_]) = new Assignable(cls)

  private def isCookieType(cls: Class[_]): Boolean = {
    classOf[AnyRef] << cls &&
      classOf[java.lang.String] !< cls &&
      classOf[java.lang.Byte] !< cls &&
      classOf[java.lang.Short] !< cls &&
      classOf[java.lang.Integer] !< cls &&
      classOf[java.lang.Long] !< cls &&
      classOf[java.lang.Float] !< cls &&
      classOf[java.lang.Double] !< cls &&
      classOf[java.lang.Character] !< cls &&
      classOf[java.lang.Boolean] !< cls &&
      classOf[java.util.Collection[_]] !< cls
  }

  private def adaptArgumentValueForCookie(argValue: AnyRef, property: ArgPropertyValue[AnyRef]): AnyRef = {
    if (property.refTypeInfo != null && property.refTypeInfo.collection) {
      // The reference collections in cookies have class String as the element type, because it is purported
      // to hold reference names that are translated to real objects during the cookie consumption.
      // It is therefore necessary to set a new empty list of strings as the cookie value for holding the reference
      // names instead of real object references.
      new java.util.ArrayList[String]
    } else argValue
  }

  private def populateCookie[Cookie <: AnyRef](propertyValues: List[ArgPropertyValue[AnyRef]],
                                               cookieClass: Class[Cookie],
                                               cookieByteCode: Array[Byte],
                                               phaseMethodArgs: Array[AnyRef]): AnyRef = {
    val cookie: Cookie = cookieClass.newInstance
    ExpressCookie.initializeCookie(cookie, cookieByteCode)

    // To populate the cookie easier create a backing map
    val beanInfo = BeanUtils.createBeanInfo(cookie)
    propertyValues.foreach(propertyValue => {
      val valueForCookie: AnyRef = adaptArgumentValueForCookie(phaseMethodArgs(propertyValue.argIndex), propertyValue)
      beanInfo.put(propertyValue.name, valueForCookie)
      // Delve into the reference for its instance as long as the default value is null.
      if (propertyValue.refTypeInfo != null)
      // The type of the reference property is changed to String so that it can hold the reference name
      // instead of the referenced object. Set the default reference name to it therefore.
        if (!propertyValue.refTypeInfo.collection) {
          beanInfo.put(propertyValue.name, propertyValue.refTypeInfo.name)
        }
      //        createRefPageWrapperConstrArgs(propertyValue, beanInfo, propertyValue.refAnnot.name) match {
      //          case Some(newRefPage) => refPages = newRefPage :: refPages
      //          case None => throw new IllegalArgumentException("Starter not found for reference '" + propertyValue.name + "' in phase " + fullyQualifiedPhaseId)
      //        }
    })

    cookie
  }

  private def cookieClassLoader(cookieClassName: String,
                                phaseMethod: Method,
                                phaseMethodArgs: Array[AnyRef]): ExpressCookieClassLoader =
    CookieClassLoaderCache(cookieClassName) match {
      case None => {
        val (startArgIndex, propList) = collectCookieProperties(phaseMethod, phaseMethodArgs)
        val metaCookie = new MetaCookie(propList, startArgIndex, false)
        val newCL = new ExpressCookieClassLoader(phaseMethod.getName, metaCookie, cookieClassName,
          starterClassResource, selectedStarterClass)
        CookieClassLoaderCache(cookieClassName, newCL)
        newCL
      }
      case Some(cookieClassLoader) => cookieClassLoader
    }

  private def collectCookieProperties(phaseMethod: Method,
                                      phaseMethodArgs: Array[AnyRef]): (Int, List[ArgPropertyValue[AnyRef]]) = {
    val startIndex = findFirstMarkerAnnotation(phaseMethod.getParameterAnnotations, 0) match {
      case None => 0
      case Some((index, firstAnnot)) => index
    }

    collectCookieProperties(phaseMethod, phaseMethodArgs, startIndex)
  }

  private def collectCookieProperties(phaseMethod: Method,
                                      phaseMethodArgs: Array[AnyRef],
                                      startIndex: Int): (Int, List[ArgPropertyValue[AnyRef]]) = {
    val argClasses = phaseMethod.getParameterTypes()

    def createProperty(index: Int, propName: String, refAnnot: Ref, genericParameters: java.lang.reflect.Type): ArgPropertyValue[AnyRef] = {
      val argType: Class[AnyRef] = argClasses(index).asInstanceOf[Class[AnyRef]]

      val refTypeInfo: PhaseMethodRefInfo = if (refAnnot == null)
        null
      else
        PhaseMethodRefInfo.newInstance(refAnnot.name, selectedStarterClass, phaseMethod.getName, index, refAnnot.col)

      val typeInfo: TypeInfo = if (refTypeInfo == null)
        PhaseMethodRefInfo.newInstance(null /*irrelevant*/ , selectedStarterClass, phaseMethod.getName, index, false /*irrelevant*/)
      else if (refTypeInfo.collection)
        // In case the reference is a collection the property type is a collection of strings.
        new DefaultTypeInfo(new ParameterizedTypeWrapper(null, argType, Array(classOf[String])))
      else
        // In the case of scalar reference the property type is of String.
        new DefaultTypeInfo(classOf[String])

      new ArgPropertyValue(index,
        propName,
        phaseMethodArgs(index),
        argType,
        typeInfo,
        refTypeInfo)
    }

    (startIndex, for {i <- List.range(0, phaseMethodArgs.length)
                      paramAnnotations = phaseMethod.getParameterAnnotations()(i);
                      genericParameterTypes = phaseMethod.getGenericParameterTypes()
                      genericParameters = genericParameterTypes(i);

                      refAnnotOpt = Utils.findAnnotation(paramAnnotations, classOf[Ref])
                      refAnnot = refAnnotOpt match {
                        case None => null
                        case Some(annot) => annot
                      }

                      propName: String = Utils.findAnnotation(paramAnnotations, classOf[Name]) match {
                        case Some(nameAnnot) => nameAnnot.value
                        case None => "parameter" + i
                      }

    } yield createProperty(i, propName, refAnnot, genericParameters))
  }

  private def findFirstMarkerAnnotation(argsAnnotations: Array[Array[java.lang.annotation.Annotation]],
                                        index: Int): Option[(Int, First)] = {
    if (index < argsAnnotations.length) {
      val argAnnotations = argsAnnotations(index)
      Utils.findAnnotation(argAnnotations, classOf[First]) match {
        case None =>
          findFirstMarkerAnnotation(argsAnnotations, index + 1)
        case Some(annot) =>
          Some((index, annot))
      }
    } else None
  }

  private def destroyInstanceAndItsReferences(instance: X) = {
    wrappedStarterInstance.destroy(instance)
    phases.foreach(phase => phase.destroyReferences)
  }

  /**
   * returns the current phase
   */
  def phase(): Phase = {
    if (currentPhaseName == null)
      doInitCallOfNewInstance

    require(currentPhaseName != null)

    findPhase(currentPhaseName) match {
      case Some(phase) => phase
      case None => throw new IllegalStateException("Cannot find phase " + currentPhaseName)
    }
  }

  private[scaladuct] def listOfPhases() = phases

  def fullyQualifiedPhaseId() = pageIdPrefix + selectedStarterClass.getSimpleName + "." + currentPhaseName

  def page(): ConfigPage[X] = phase.firstPage

  class MethodProxyHolder(val proxy: MethodProxy) extends Throwable

  private[this] val methodProxyCaptureLock = new ReentrantLock
  private[this] var captureMethodProxyMode = false

  def enterMethodProxyCaptureMode() {
    methodProxyCaptureLock.lock
  }

  def leaveMethodProxyCaptureMode() {
    methodProxyCaptureLock.unlock
  }

  @serializable
  @SerialVersionUID(1L)
  object Phase {
    def createConfigurablePhase(phaseName: String, method: Method, methodProxy: MethodProxy,
                                phaseMethodArgs: Array[AnyRef], metaCookie: MetaCookie, cookie: AnyRef,
                                nameOfBundle: String, instance: AnyRef) = {

      // try to get a metadata source for the current phase from the starter 
      val metaDataSourceFromStarter = if (instance.isInstanceOf[HasHintsMetaDataSourceProvider]) {
        val instanceAsMetaDataProvider = instance.asInstanceOf[HasHintsMetaDataSourceProvider]
        instanceAsMetaDataProvider.configPageMetaDataSource(phaseName) match {
          case None => null
          case Some(metaDataSource) => metaDataSource
        }
      } else null

      val listsManagerFromStarter = if (instance.isInstanceOf[ListsSource]) {
        val instanceAsListsSource = instance.asInstanceOf[ListsSource]
        instanceAsListsSource.listsForPhase(phaseName) match {
          case None => null
          case Some(listsManager) => listsManager
        }
      } else null

      new Phase(phaseName, method, methodProxy, phaseMethodArgs, instance) {
        phases = this :: phases

        val firstPage = new StackConfigPage[X](new PhaseMethodInvokingPage,
          List(new SingletonStackPageLeafFactory(
            if (metaDataSourceFromStarter == null)
              new InputCollectorPage(metaCookie, cookie)
                with ResourceBundleHasHintsMetaDataSource
                with AbstractL10nProperties
                with DefaultListsManager {
                val lookupFn = configPageFnLookup
                val bundleName = nameOfBundle
                val bundlePrefix = ""
                val pageNameInBundle = method.getName
                val primaryListsManager = listsManagerFromStarter
              }
            else
              new InputCollectorPage(metaCookie, cookie)
                with DuplexHasHintsMetaDataSource
                with DefaultListsManager {
                val lookupFn = configPageFnLookup
                val firstConfigPageMetaDataSource = metaDataSourceFromStarter
                val secondConfigPageMetaDataSource = new ResourceBundleHasHintsMetaDataSource
                  with AbstractL10nProperties {
                  val bundleName = nameOfBundle
                  val bundlePrefix = ""
                  val pageNameInBundle = method.getName
                }
                val primaryListsManager = listsManagerFromStarter
              }))
        )
      }
    }

    def createNonConfigurablePhase(instance: AnyRef, phaseName: String, method: Method, methodProxy: MethodProxy,
                                   phaseMethodArgs: Array[AnyRef], primaryInstance: X) = {
      new Phase(phaseName, method, methodProxy, phaseMethodArgs, instance) {
        val firstPage = new NoConfigPage(primaryInstance)
      }
    }
  }

  @serializable
  @SerialVersionUID(1L)
  abstract class Phase(val phaseName: String,
                       @transient private var m: Method,
                       @transient private var mProxy: MethodProxy,
                       val phaseMethodArgs: Array[AnyRef],
                       val instance: AnyRef) {
    private val methodName = m.getName
    private val methodArgTypes = m.getParameterTypes

    val firstPage: ConfigPage[X]

    private[this] var createdInstances: List[(Starter[Any], Any)] = Nil

    val fullyQualifiedId = fullyQualifiedPhaseId()

    def reinitMethod() = {
      this.m = instance.getClass.getMethod(methodName, methodArgTypes: _*)
    }

    def reinitMethodProxy() = {
      enterMethodProxyCaptureMode
      captureMethodProxyMode = true
      try {
        method.invoke(instance, phaseMethodArgs: _*)
      } catch {
        case invExc: InvocationTargetException => {
          invExc.getTargetException match {
            case proxyHolder: MethodProxyHolder => this.mProxy = proxyHolder.proxy
            case _ => throw invExc
          }
        }
      }
      finally {
        captureMethodProxyMode = false
        leaveMethodProxyCaptureMode
      }
    }

    def method: Method = {
      if (m == null)
        reinitMethod()
      m
    }

    def methodProxy: MethodProxy = {
      if (mProxy == null)
        reinitMethodProxy()
      mProxy
    }

    def registerCreatedInstance(starter: Starter[Any], instance: Any) {
      createdInstances ::= (starter, instance)
    }

    def destroyReferences() {
      //refPageResults.values.foreach(refPageResult => refPageResult.destroy)
      //createdInstances.foreach((starter, instance) => starter.destroy(instance))
      createdInstances.foreach(ci => ci._1.destroy(ci._2))
    }

    def cancelPhase() = {
      phases = phases.tail
    }

    @serializable
    @SerialVersionUID(1L)
    class NoConfigPage(primaryInstance: X) extends {
    val id = pageIdPrefix + selectedStarterClass.getName + "." + phaseName + "#NoConfig"
    } with ConfigPage[X] with ExtensibleDecorator {
      type Cookie = Terminal
      private[this] var invalidInstance: Boolean = false
      private[this] var instance = primaryInstance


      def decoratedForExtensible = wrappedStarterInstance

      def getCookie() = Terminal

      def consume(cookie: Cookie): NewStarter[X] = {
        assert(cookie == Terminal)

        if (invalidInstance) {
          instance = methodProxy.invokeSuper(wrappedStarterInstance, phaseMethodArgs).asInstanceOf[X]
          invalidInstance = false
        }

        NewStarter(new InstanceStarter(instance,
          methodProxy.invokeSuper(wrappedStarterInstance, phaseMethodArgs).asInstanceOf[X]))
      }

      override def unconsume(result: ConfigResult[X]) = {
        // Call the consume method if any exists
        // todo:

        result match {
          case NewStarter(starter) => {
            assume(starter.isInstanceOf[InstanceStarter])
            // Un-instantiate the instance which is the product of the newInstance method
            starter.asInstanceOf[InstanceStarter].destroy
            invalidInstance = true
          }
          case _ => error("unexpected result for NoConfigPage")
        }
      }

      override def canUnconsume(result: ConfigResult[X]): Boolean = {
        // todo: check the Unconsume annotation on the newInstance method whether it prohibits the unconsumption
        true
      }
    }

    ///////////////// Pages

    @serializable
    @SerialVersionUID(1L)
    class InputCollectorPage[H](val metaCookie: MetaCookie, cookie: AnyRef) extends StackPageLeaf
    with HasHints[H] with ExtensibleDecorator {
      val id = pageIdPrefix + selectedStarterClass.getName + "." + phaseName
      type Cookie = AnyRef

      def decoratedForExtensible = wrappedStarterInstance

      val hintsDelegate: HasHints[H] = {
        HintsHelper.extract(method, cookie, wrappedStarterInstance).asInstanceOf[HasHints[H]]
      }
      type HintType = hintsDelegate.HintType

      def getCookie() = cookie

      def consume(receivedCookie: Cookie): List[StackPageLeafFactory] = {
        val beanAsMap = BeanUtils.createBeanInfo(receivedCookie)

        var refPages: List[StackPageLeafFactory] = Nil
        if (metaCookie.isCookieArgument) {
          phaseMethodArgs(0) = receivedCookie
        } else {
          metaCookie.cookieProperties.foreach(property => {
            beanAsMap.get(property.name) match {
              case Some(value) => {
                if (property.refTypeInfo != null) {
                  // the argument to FunctionalStackPageLeafFactory is a function
                  if (value.isInstanceOf[java.util.Collection[String]]) {
                    // the value of the argument represents a list of reference names
                    val refNamesIterator = value.asInstanceOf[java.util.Collection[String]].iterator
                    while (refNamesIterator.hasNext) {
                      val refName = refNamesIterator.next
                      refPages ::= new FunctionalStackPageLeafFactory(
                        createRefPage(refName, property.refTypeInfo.getType, property.argIndex, true))
                    }
                  } else {
                    refPages ::= new FunctionalStackPageLeafFactory(
                      createRefPage(value.asInstanceOf[String],
                        metaCookie.property(property.name) match {
                          case None => error("property not found:" + property.name)
                          //case Some(refMetaProp) => refMetaProp.clazz
                          case Some(refMetaProp) => refMetaProp.refTypeInfo.getType
                        }, property.argIndex, false))
                  }
                } else {
                  phaseMethodArgs(property.argIndex) = value
                }
              }
              case None =>
            }
          })
        }

        refPages
      }

      private def createRefPage[Y >: Null <: AnyRef](refName: String, refType: java.lang.reflect.Type, argIndex: Int,
                                                     isCollection: Boolean): ReferencePage[Y] = {
        configPageFnLookup(refName) match {
          case List(value) => {
            val refConfigPageFn: ConfigPageFn = value.asInstanceOf[ConfigPageFn]
            val refPage: ConfigPage[_] = refConfigPageFn(refType)
            new ReferencePage[Y](refPage.asInstanceOf[ConfigPage[Y]], argIndex, isCollection)
          }
          case List(_, _, _*) => error("more options for config page referenced by name " + refName)
          case Nil => error("config page not found for reference name " + refName)
        }
      }

      def canUnconsume(result: List[StackPageLeafFactory]) = true

      def unconsume(result: List[StackPageLeafFactory]) = null

      def hintPage(hint: Property[HintType]) = hintsDelegate.hintPage(hint)

      def hints(subject: String) = hintsDelegate.hints(subject)

      def cancel() = cancelPhase
    }

    @serializable
    @SerialVersionUID(1L)
    class ReferencePage[Y >: Null <: AnyRef](val refPage: ConfigPage[Y],
                                             val argIndex: Int,
                                             val isRefCollection: Boolean) extends StackPageLeaf with ExtensibleDecorator {
      type Cookie = refPage.Cookie
      lazy val id = refPage.id

      def decoratedForExtensible = wrappedStarterInstance

      def getCookie() = refPage.getCookie

      private[this] var refInstance: Y = null
      private[this] var refStarter: Starter[Y] = null

      def consume(cookie: Cookie): List[StackPageLeafFactory] = {
        val refPageResult: ConfigResult[Y] = refPage.consume(cookie)
        refPageResult match {
          case NextPage(nextRefPage) => {
            val nextPage = new ReferencePage[Y](nextRefPage, argIndex, isRefCollection)
            List(new SingletonStackPageLeafFactory(nextPage))
          }
          case NewStarter(starter) => {
            refInstance = starter.newInstance
            refStarter = starter
            registerCreatedInstance(starter.asInstanceOf[Starter[Any]], refInstance)
            if (isRefCollection)
              phaseMethodArgs(argIndex).asInstanceOf[java.util.Collection[Any]].add(refInstance)
            else
              phaseMethodArgs(argIndex) = refInstance

            Nil
          }
        }
      }

      def canUnconsume(result: List[StackPageLeafFactory]) = {
        if (result == Nil) {
          refPage.canUnconsume(NewStarter(refStarter))
        } else {
          val nextRefPageFactory = result(0).asInstanceOf[SingletonStackPageLeafFactory]
          val nextRefPage = nextRefPageFactory.leaf.asInstanceOf[ReferencePage[Y]]
          refPage.canUnconsume(NextPage(nextRefPage.refPage))
        }
      }

      def unconsume(result: List[StackPageLeafFactory]) = {
        if (result == Nil) {
          refStarter.destroy(refInstance)
          refPage.unconsume(NewStarter(refStarter))
        } else {
          val nextRefPageFactory = result(0).asInstanceOf[SingletonStackPageLeafFactory]
          val nextRefPage = nextRefPageFactory.leaf.asInstanceOf[ReferencePage[Y]]
          refPage.unconsume(NextPage(nextRefPage.refPage))
        }
      }

      override def extension[T](extClass: Class[T]) = {
        super.extension(extClass) match {
          case None => refPage.extension(extClass)
          case Some(ext) => Some(ext)
        }
      }

      def cancel() = {
        refPage.cancel
      }
    }

    @serializable
    @SerialVersionUID(1L)
    class PhaseMethodInvokingPage extends {
    val id = pageIdPrefix + selectedStarterClass.getName + "." + phaseName + "#Invoker"
    } with ConfigPage[X] with Logged with ExtensibleDecorator {
      type Cookie = Terminal

      def decoratedForExtensible = wrappedStarterInstance

      def getCookie() = Terminal

      def consume(cookie: Terminal): ConfigResult[X] = {
        //      // Optimization: remove the phase from the phases list if it contains no references
        //      if (removeUnusedPhases) {
        //        phases = phases.filterNot(p => p.phaseName == phaseName && p.isVolatile)
        //      }

        try {
          val instance: X = methodProxy.invokeSuper(wrappedStarterInstance, phaseMethodArgs).asInstanceOf[X]
          NewStarter(new InstanceStarter(instance,
            methodProxy.invokeSuper(wrappedStarterInstance, phaseMethodArgs).asInstanceOf[X]))
        }
        catch {
          case suspendedCall: SuspendedCallSignal[_] => {
            // the state of the interceptor has moved to the next phase
            val nextPage: ConfigPage[X] = phase().firstPage
            NextPage(nextPage)
          }
        }
      }

      override def canUnconsume(result: ConfigResult[X]): Boolean = {
        // todo: check the Unconsume annotation on the phase method whether it prohibits the unconsumption
        true
      }

      override def unconsume(result: ConfigResult[X]) = {
        // Call the un... method if any
        val unMethodName: String = "un" + method.getName
        try {
          val unmethod: Method = method.getDeclaringClass.getMethod(unMethodName, method.getParameterTypes: _*)
          debug("unmethod " + unMethodName + " found")
          unmethod.invoke(wrappedStarterInstance, phaseMethodArgs: _*)
        }
        catch {
          case nsme: NoSuchMethodException => {

            // no-arg version attempt
            try {
              val unmethod: Method = method.getDeclaringClass.getMethod(unMethodName)
              debug("unmethod " + unMethodName + " found")
              unmethod.invoke(wrappedStarterInstance)
            }
            catch {
              case nsme: NoSuchMethodException => debug("unmethod " + unMethodName + " not available")
            }

          }
        }

        result match {
          case NewStarter(starter) => {
            assume(starter.isInstanceOf[Extensible])

            starter.asInstanceOf[Extensible].extension(classOf[InstanceStarter]) match {
              case None => error("Expected InstanceStarter instance");
              case Some(instanceStarter) => {
                // Un-instantiate the instance which is the product of the last phase method
                instanceStarter.asInstanceOf[InstanceStarter].destroy()
              }
            }
          }
          case NextPage(nextPage) => {
          }
        }
      }

    }

  }

  @serializable
  @SerialVersionUID(1L)
  private class InstanceStarter(val firstInstance: X, nextInstanceGetter: => X)
    extends Starter[X] with ExtensibleDecorator {
    private[this] var isFirst = true

    val outer = PhaseMethodInterceptor.this

    def decoratedForExtensible = wrappedStarterInstance

    def newInstance() = if (isFirst) {
      isFirst = false
      firstInstance
    } else {
      nextInstanceGetter
    }

    def destroy(instance: X): Unit = {
      destroyInstanceAndItsReferences(instance)
    }

    def destroy(): Unit = {
      destroy(firstInstance)
    }

  }

  def configure(moderator: (ConfigPage[X]) => AnyRef): Starter[X] = {
    new ConfigPageGateway(phase.firstPage).configure(moderator)
  }

  def newInstance(moderator: (ConfigPage[X]) => AnyRef): X = {
    val starter: Starter[X] = configure(moderator)
    starter.newInstance
  }
}

private object CookieClassLoaderCache {
  private val cookieCLCache: Map[String, ExpressCookieClassLoader] = Map.synchronized(Map.empty)

  def apply(cookieClassName: String, classLoader: ExpressCookieClassLoader) {
    cookieCLCache += (cookieClassName -> classLoader)
  }

  def apply(cookieClassName: String): Option[ExpressCookieClassLoader] = {
    cookieCLCache.get(cookieClassName)
  }
}
